package com.airbnb.lottie.animation.content;

import com.airbnb.lottie.LottieProperty;
import com.airbnb.lottie.animation.keyframe.BaseKeyframeAnimation;
import com.airbnb.lottie.model.KeyPath;
import com.airbnb.lottie.utils.MiscUtils;
import com.airbnb.lottie.LottieDrawable;
import com.airbnb.lottie.model.content.PolystarShape;
import com.airbnb.lottie.model.content.ShapeTrimPath;
import com.airbnb.lottie.model.layer.BaseLayer;
import com.airbnb.lottie.value.LottieValueCallback;

import ohos.agp.render.Path;
import ohos.agp.utils.Point;
import org.jetbrains.annotations.Nullable;

import java.util.List;

public class PolystarContent implements PathContent, BaseKeyframeAnimation.AnimationListener, KeyPathElementContent {
    /**
     * This was empirically derived by creating polystars, converting them to
     * curves, and calculating a scale factor.
     * It works best for polygons and stars with 3 points and needs more
     * work otherwise.
     */
    private static final float POLYSTAR_MAGIC_NUMBER = .47829f;
    private static final float POLYGON_MAGIC_NUMBER = .25f;
    private final Path path = new Path();

    private final String name;
    private final LottieDrawable lottieDrawable;
    private final PolystarShape.Type type;
    private final boolean hidden;
    private final BaseKeyframeAnimation<?, Float> pointsAnimation;
    private final BaseKeyframeAnimation<?, Point> positionAnimation;
    private final BaseKeyframeAnimation<?, Float> rotationAnimation;
    @Nullable
    private final BaseKeyframeAnimation<?, Float> innerRadiusAnimation;
    private final BaseKeyframeAnimation<?, Float> outerRadiusAnimation;
    @Nullable private final BaseKeyframeAnimation<?, Float> innerRoundednessAnimation;
    private final BaseKeyframeAnimation<?, Float> outerRoundednessAnimation;

    private CompoundTrimPathContent trimPaths = new CompoundTrimPathContent();
    private boolean isPathValid;

    public PolystarContent(LottieDrawable lottieDrawable, BaseLayer layer, PolystarShape polystarShape) {
        this.lottieDrawable = lottieDrawable;

        name = polystarShape.getName();
        type = polystarShape.getType();
        hidden = polystarShape.isHidden();
        pointsAnimation = polystarShape.getPoints().createAnimation();
        positionAnimation = polystarShape.getPosition().createAnimation();
        rotationAnimation = polystarShape.getRotation().createAnimation();
        outerRadiusAnimation = polystarShape.getOuterRadius().createAnimation();
        outerRoundednessAnimation = polystarShape.getOuterRoundedness().createAnimation();
        if (type == PolystarShape.Type.STAR) {
            innerRadiusAnimation = polystarShape.getInnerRadius().createAnimation();
            innerRoundednessAnimation = polystarShape.getInnerRoundedness().createAnimation();
        } else {
            innerRadiusAnimation = null;
            innerRoundednessAnimation = null;
        }

        layer.addAnimation(pointsAnimation);
        layer.addAnimation(positionAnimation);
        layer.addAnimation(rotationAnimation);
        layer.addAnimation(outerRadiusAnimation);
        layer.addAnimation(outerRoundednessAnimation);
        if (type == PolystarShape.Type.STAR) {
            layer.addAnimation(innerRadiusAnimation);
            layer.addAnimation(innerRoundednessAnimation);
        }

        pointsAnimation.addUpdateListener(this);
        positionAnimation.addUpdateListener(this);
        rotationAnimation.addUpdateListener(this);
        outerRadiusAnimation.addUpdateListener(this);
        outerRoundednessAnimation.addUpdateListener(this);
        if (type == PolystarShape.Type.STAR) {
            innerRadiusAnimation.addUpdateListener(this);
            innerRoundednessAnimation.addUpdateListener(this);
        }
    }

    @Override
    public void onValueChanged() {
        invalidate();
    }

    private void invalidate() {
        isPathValid = false;
        lottieDrawable.invalidateSelf();
    }

    @Override
    public void setContents(List<Content> contentsBefore, List<Content> contentsAfter) {
        for (int i = 0; i < contentsBefore.size(); i++) {
            Content content = contentsBefore.get(i);
            if (content instanceof TrimPathContent
                    && ((TrimPathContent) content).getType() == ShapeTrimPath.Type.SIMULTANEOUSLY) {
                TrimPathContent trimPath = (TrimPathContent) content;
                trimPaths.addTrimPath(trimPath);
                trimPath.addListener(this);
            }
        }
    }

    @Override
    public Path getPath() {
        if (isPathValid) {
            return path;
        }

        path.reset();

        if (hidden) {
            isPathValid = true;
            return path;
        }

        switch (type) {
            case STAR:
                createStarPath();
                break;
            case POLYGON:
                createPolygonPath();
                break;
        }

        path.close();

        trimPaths.apply(path);

        isPathValid = true;
        return path;
    }

    @Override
    public String getName() {
        return name;
    }

    private void createStarPath() {
        float points = pointsAnimation.getValue();
        double currentAngle = rotationAnimation == null ? 0f : rotationAnimation.getValue();
        // Start at +y instead of +x
        currentAngle -= 90;
        // convert to radians
        currentAngle = Math.toRadians(currentAngle);
        // adjust current angle for partial points
        float anglePerPoint = (float) (2 * Math.PI / points);
        float halfAnglePerPoint = anglePerPoint / 2.0f;
        float partialPointAmount = points - (int) points;
        if (partialPointAmount != 0) {
            currentAngle += halfAnglePerPoint * (1f - partialPointAmount);
        }

        float outerRadius = outerRadiusAnimation.getValue();
        float innerRadius = innerRadiusAnimation.getValue();

        float innerRoundedness = 0f;
        if (innerRoundednessAnimation != null) {
            innerRoundedness = innerRoundednessAnimation.getValue() / 100f;
        }
        float outerRoundedness = 0f;
        if (outerRoundednessAnimation != null) {
            outerRoundedness = outerRoundednessAnimation.getValue() / 100f;
        }

        float x;
        float y;
        float previousX;
        float previousY;
        float partialPointRadius = 0;
        if (partialPointAmount != 0) {
            partialPointRadius = innerRadius + partialPointAmount * (outerRadius - innerRadius);
            x = (float) (partialPointRadius * Math.cos(currentAngle));
            y = (float) (partialPointRadius * Math.sin(currentAngle));
            path.moveTo(x, y);
            currentAngle += anglePerPoint * partialPointAmount / 2f;
        } else {
            x = (float) (outerRadius * Math.cos(currentAngle));
            y = (float) (outerRadius * Math.sin(currentAngle));
            path.moveTo(x, y);
            currentAngle += halfAnglePerPoint;
        }

        // True means the line will go to outer radius. False means inner radius.
        boolean longSegment = false;
        double numPoints = Math.ceil(points) * 2;
        for (int i = 0; i < numPoints; i++) {
            float radius = longSegment ? outerRadius : innerRadius;
            float dTheta = halfAnglePerPoint;
            if (partialPointRadius != 0 && i == numPoints - 2) {
                dTheta = anglePerPoint * partialPointAmount / 2f;
            }
            if (partialPointRadius != 0 && i == numPoints - 1) {
                radius = partialPointRadius;
            }
            previousX = x;
            previousY = y;
            x = (float) (radius * Math.cos(currentAngle));
            y = (float) (radius * Math.sin(currentAngle));

            if (innerRoundedness == 0 && outerRoundedness == 0) {
                path.lineTo(x, y);
            } else {
                float cp1Theta = (float) (Math.atan2(previousY, previousX) - Math.PI / 2f);
                float cp1Dx = (float) Math.cos(cp1Theta);
                float cp1Dy = (float) Math.sin(cp1Theta);

                float cp2Theta = (float) (Math.atan2(y, x) - Math.PI / 2f);
                float cp2Dx = (float) Math.cos(cp2Theta);
                float cp2Dy = (float) Math.sin(cp2Theta);

                float cp1Roundedness = longSegment ? innerRoundedness : outerRoundedness;
                float cp2Roundedness = longSegment ? outerRoundedness : innerRoundedness;
                float cp1Radius = longSegment ? innerRadius : outerRadius;
                float cp2Radius = longSegment ? outerRadius : innerRadius;

                float cp1x = cp1Radius * cp1Roundedness * POLYSTAR_MAGIC_NUMBER * cp1Dx;
                float cp1y = cp1Radius * cp1Roundedness * POLYSTAR_MAGIC_NUMBER * cp1Dy;
                float cp2x = cp2Radius * cp2Roundedness * POLYSTAR_MAGIC_NUMBER * cp2Dx;
                float cp2y = cp2Radius * cp2Roundedness * POLYSTAR_MAGIC_NUMBER * cp2Dy;
                if (partialPointAmount != 0) {
                    if (i == 0) {
                        cp1x *= partialPointAmount;
                        cp1y *= partialPointAmount;
                    } else if (i == numPoints - 1) {
                        cp2x *= partialPointAmount;
                        cp2y *= partialPointAmount;
                    }
                }

                Point point1 = new Point(previousX - cp1x, previousY - cp1y);
                Point point2 = new Point(x + cp2x, y + cp2y);
                Point point3 = new Point(x, y);
                path.cubicTo(point1, point2, point3);
            }

            currentAngle += dTheta;
            longSegment = !longSegment;
        }

        Point position = positionAnimation.getValue();
        path.offset(position.getPointX(), position.getPointY());
        path.close();
    }

    private void createPolygonPath() {
        int points = (int) Math.floor(pointsAnimation.getValue());
        double currentAngle = rotationAnimation == null ? 0f : rotationAnimation.getValue();
        // Start at +y instead of +x
        currentAngle -= 90;
        // convert to radians
        currentAngle = Math.toRadians(currentAngle);
        // adjust current angle for partial points
        float anglePerPoint = (float) (2 * Math.PI / points);

        float roundedness = outerRoundednessAnimation.getValue() / 100f;
        float radius = outerRadiusAnimation.getValue();
        float x;
        float y;
        float previousX;
        float previousY;
        x = (float) (radius * Math.cos(currentAngle));
        y = (float) (radius * Math.sin(currentAngle));
        path.moveTo(x, y);
        currentAngle += anglePerPoint;

        double numPoints = Math.ceil(points);
        for (int i = 0; i < numPoints; i++) {
            previousX = x;
            previousY = y;
            x = (float) (radius * Math.cos(currentAngle));
            y = (float) (radius * Math.sin(currentAngle));

            if (roundedness != 0) {
                float cp1Theta = (float) (Math.atan2(previousY, previousX) - Math.PI / 2f);
                float cp1Dx = (float) Math.cos(cp1Theta);
                float cp1Dy = (float) Math.sin(cp1Theta);

                float cp2Theta = (float) (Math.atan2(y, x) - Math.PI / 2f);
                float cp2Dx = (float) Math.cos(cp2Theta);
                float cp2Dy = (float) Math.sin(cp2Theta);

                float cp1x = radius * roundedness * POLYGON_MAGIC_NUMBER * cp1Dx;
                float cp1y = radius * roundedness * POLYGON_MAGIC_NUMBER * cp1Dy;
                float cp2x = radius * roundedness * POLYGON_MAGIC_NUMBER * cp2Dx;
                float cp2y = radius * roundedness * POLYGON_MAGIC_NUMBER * cp2Dy;

                Point point1 = new Point(previousX - cp1x, previousY - cp1y);
                Point point2 = new Point(x + cp2x, y + cp2y);
                Point point3 = new Point(x, y);
                path.cubicTo(point1, point2, point3);

            } else {
                path.lineTo(x, y);
            }

            currentAngle += anglePerPoint;
        }

        Point position = positionAnimation.getValue();
        path.offset(position.getPointX(), position.getPointY());
        path.close();
    }

    @Override
    public void resolveKeyPath(KeyPath keyPath, int depth, List<KeyPath> accumulator, KeyPath currentPartialKeyPath) {
        MiscUtils.resolveKeyPath(keyPath, depth, accumulator, currentPartialKeyPath, this);
    }

    @Override
    public <T> void addValueCallback(T property, @Nullable LottieValueCallback<T> callback) {
        if (property == LottieProperty.POLYSTAR_POINTS) {
            pointsAnimation.setValueCallback((LottieValueCallback<Float>) callback);
        } else if (property == LottieProperty.POLYSTAR_ROTATION) {
            rotationAnimation.setValueCallback((LottieValueCallback<Float>) callback);
        } else if (property == LottieProperty.POSITION) {
            positionAnimation.setValueCallback((LottieValueCallback<Point>) callback);
        } else if (property == LottieProperty.POLYSTAR_INNER_RADIUS && innerRadiusAnimation != null) {
            innerRadiusAnimation.setValueCallback((LottieValueCallback<Float>) callback);
        } else if (property == LottieProperty.POLYSTAR_OUTER_RADIUS) {
            outerRadiusAnimation.setValueCallback((LottieValueCallback<Float>) callback);
        } else if (property == LottieProperty.POLYSTAR_INNER_ROUNDEDNESS && innerRoundednessAnimation != null) {
            innerRoundednessAnimation.setValueCallback((LottieValueCallback<Float>) callback);
        } else if (property == LottieProperty.POLYSTAR_OUTER_ROUNDEDNESS) {
            outerRoundednessAnimation.setValueCallback((LottieValueCallback<Float>) callback);
        }
    }
}
